﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web.Mvc;

namespace HashFoo.Rest.Server
{
    /// <summary>
    /// By default requires authorization on all actions.  Allows controllers and namespaces to be excepted.
    /// </summary>
    /// <typeparam name="TRole"></typeparam>
    public class GlobalAuthorizeFilter<TRole> : IAuthorizationFilter
    {
        readonly Func<TRole, string> _roleNameProvider;

        readonly Dictionary<Type, IEnumerable<string>> _roleRequirements = new Dictionary<Type, IEnumerable<string>>();
        List<Type> _exemptControllers = new List<Type>();

        /// <summary>
        /// Create the authentication filter.
        /// </summary>
        /// <param name="roleNameProvider">Takes a <typeparamref name="TRole"/>  object and returns a
        /// string that the ASP role provider can understand.</param>
        public GlobalAuthorizeFilter(Func<TRole, string> roleNameProvider)
        {
            _roleNameProvider = roleNameProvider;
        }

        /// <summary>
        /// Excempts a controller from the global authorization scheme.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> ExceptController<T>() where T : IController
        {
            return ExceptControllers(typeof(T));
        }

        /// <summary>
        /// Exempts a group of controllers from the global authorization scheme.
        /// </summary>
        /// <param name="controllerTypes"></param>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> ExceptControllers(params Type[] controllerTypes)
        {
            return ExceptControllers(controllerTypes.AsEnumerable());
        }

        /// <summary>
        /// Exempts a group of controllers from the global authorization scheme.
        /// </summary>
        /// <param name="controllerTypes"></param>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> ExceptControllers(IEnumerable<Type> controllerTypes)
        {
            _exemptControllers.AddRange(
                controllerTypes.Where(t => t.GetInterfaces().Any(i => i == typeof(IController))).Distinct());

            _exemptControllers = _exemptControllers.Distinct().ToList();
            return this;
        }

        /// <summary>
        /// Exempts and entire namespace from the global authorization scheme.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> ExeceptNamespaceOf<T>() where T : IController
        {
            return ExceptControllers(AllControllersInNamespaceOf<T>());
        }

        /// <summary>
        /// Runs the authorization filter for a specific context.
        /// </summary>
        /// <param name="filterContext"></param>
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            // only run global auth if there isn't a local Authorize attribute around.
            var actionAuth = filterContext.ActionDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).FirstOrDefault();
            if (actionAuth != null) return;

            var controllerType = filterContext.Controller.GetType();

            var controllerAuth = controllerType.GetCustomAttributes(typeof(AuthorizeAttribute), true).FirstOrDefault();
            if (controllerAuth != null) return;

            if (_exemptControllers.Any(t => t == controllerType)) return;

            // check if the controller or the action is marked as OpenAccess
            var isOpenAccessController = controllerType.GetCustomAttributes(typeof(OpenAccess), true).FirstOrDefault() != null;
            if (isOpenAccessController) return;

            var isOpenAccessAction = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OpenAccess), true).FirstOrDefault() != null;
            if (isOpenAccessAction) return;

            var roles = _roleRequirements.ContainsKey(controllerType) ? _roleRequirements[controllerType] : null;

            var authorizer = new AuthorizeAttribute
            {
                Roles = roles == null ? null : roles.Select(r => r.ToLowerInvariant()).Distinct().Aggregate((c, n) => c + ", " + n)
            };

            authorizer.OnAuthorization(filterContext);
        }

        /// <summary>
        /// Adds a role requirement to a controller.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="roles"></param>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> WithRolesForController<T>(params TRole[] roles) where T : IController
        {
            return WithRolesForControllers(new[] { typeof(T) }, roles);
        }

        /// <summary>
        /// Adds a set (logic OR) of role requirements to a group of controllers.
        /// </summary>
        /// <param name="controllerTypes"></param>
        /// <param name="roles"></param>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> WithRolesForControllers(IEnumerable<Type> controllerTypes, IEnumerable<TRole> roles)
        {
            controllerTypes = controllerTypes.ToArray();
            if (controllerTypes.Any(t => _exemptControllers.Contains(t))) throw new ConfigurationErrorsException("Cannot authorize a controller that is already exempt.");

            var roleNames = roles.Select(_roleNameProvider).ToArray();
            foreach (var controllerType in controllerTypes)
            {
                if (_roleRequirements.ContainsKey(controllerType))
                {
                    var existingRoles = _roleRequirements[controllerType];
                    _roleRequirements[controllerType] = existingRoles.Union(roleNames);
                }
                else _roleRequirements.Add(controllerType, roleNames);
            }

            return this;
        }

        /// <summary>
        /// Adds a set (logical OR) of role requirements to a specific namespace.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="roles"></param>
        /// <returns></returns>
        public GlobalAuthorizeFilter<TRole> WithRolesForNamespaceOf<T>(params TRole[] roles) where T : IController
        {
            return WithRolesForControllers(AllControllersInNamespaceOf<T>(), roles);
        }

        static IEnumerable<Type> AllControllersInNamespaceOf<T>() where T : IController
        {
            var controllerType = typeof(T);
            if (controllerType.Namespace == null) throw new InvalidOperationException("Controller T must have a namespace");
            var controllerAssembly = controllerType.Assembly;

            var allControllersInNamespace =
                controllerAssembly.GetTypes()
                    .Where(t =>
                           t.Namespace != null &&
                           t.Namespace.Contains(controllerType.Namespace) &&
                           t.GetInterfaces().Any(i => i == typeof(IController)));
            return allControllersInNamespace;
        }
    }
}